﻿using BouncingBallGame.Helpers;
using BouncingBallGame.ViewModels.ShapeViewModels;
using Dpy.MathWinRT.Geometry.Geometry2D;
using Dpy.MathWinRT;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using BouncingBallGame.Models;
using BouncingBallGame.DataModel;
using System.Windows.Input;
using Windows.UI.Popups;

namespace BouncingBallGame.ViewModels
{
    public class PlayGroundVM : Common.BindableBase, IScalableViewModel, IDisposable
    {
        #region Fields

        private const double TimerSecondsInterval = 0.02;
        private readonly DispatcherTimer timer = new DispatcherTimer();
        double height;
        double width;
        string name;
        private Brush background;
        private GameLevelModel gameLevel;
        private double score;
        private ICommand restartCommand;
        private ICommand saveCommand;
        private ICommand loadCommand;
        private readonly PageManager pageManager = new PageManager();

        private bool shouldAddGravity = false;

        #endregion


        #region Properties

        public PageManager PageManager
        {
            get
            {
                return this.pageManager;
            }
        }

        public string Description
        {
            get
            {
                if (this.GameLevel == null)
                {
                    return string.Empty;
                }

                StringBuilder description = new StringBuilder();

                description.AppendFormat("Level name: {0}", this.GameLevel.Name);
                description.AppendLine();
                description.AppendFormat("Level height: {0} units", this.GameLevel.Height);
                description.AppendLine();
                description.AppendFormat("Level width: {0} units", this.GameLevel.Width);
                description.AppendLine();
                description.AppendFormat("Level gravity acceleration: {0} units/(s*s)", this.GameLevel.GravityAcceleration);
                description.AppendLine();
                description.AppendFormat("Ball diameter: {0} units", this.GameLevel.Ball.Radius * 2);
                description.AppendLine();
                description.AppendFormat("Ball normal collision energy loss: {0}%", this.GameLevel.Ball.NormalCollisionSpeedLoss * 100);
                description.AppendLine();
                description.AppendFormat("Ball tangent collision energy loss: {0}%", this.GameLevel.Ball.TangentCollisionSpeedLoss * 100);
                description.AppendLine();
                description.AppendFormat("Aim of the game: Collect all points with minimal ball interactions!");

                return description.ToString();
            }
        }

        public GameLevelModel GameLevel 
        {
            get
            {
                return this.gameLevel;
            }
            set
            {
                this.LoadGameLevel(value);
            }
        }

        public double Score
        {
            get
            {
                return this.score;
            }
            set
            {
                if (this.score != value)
                {
                    this.score = value;
                    this.OnPropertyChanged("Score");
                }
            }
        }

        public List<GiftViewModel> Gifts { get; private set; }

        public ObstacleViewModel Playground { get; private set; }

        public List<ObstacleViewModel> Obstacles { get; private set; }
                
        public BallViewModel Ball { get; private set; }

        private Vector2D GravityAcceleration { get; set; }

        public ObservableCollection<ShapeViewModelBase> DrawableObjects { get; set; }

        public bool IsInMove
        {
            get
            {
                return this.timer.IsEnabled;
            }
        }

        public string Name
        {
            get
            {
                return this.name;
            }
            set
            {
                if (this.name != value)
                {
                    this.name = value;
                    this.OnPropertyChanged("Name");
                }
            }
        }

        public double Height
        {
            get
            {
                return this.height;
            }
            set
            {
                if (this.height != value)
                {
                    this.height = value;
                    this.OnPropertyChanged("Height");
                }
            }
        }

        public double Width
        {
            get
            {
                return this.width;
            }
            set
            {
                if (this.width != value)
                {
                    this.width = value;
                    this.OnPropertyChanged("Width");
                }
            }
        }

        public Brush Background
        {
            get
            {
                return this.background;
            }
            set
            {
                if (this.background != value)
                {
                    this.background = value;
                    this.OnPropertyChanged("Background");
                }
            }
        }

        public ICommand Restart
        {
            get
            {
                if (this.restartCommand == null)
                {
                    this.restartCommand = new RelayCommand(this.HandleRestartCommand);
                }

                return this.restartCommand;
            }
        }

        public ICommand Save
        {
            get
            {
                if (this.saveCommand == null)
                {
                    this.saveCommand = new RelayCommand(this.HandleSaveCommand);
                }

                return this.saveCommand;
            }
        }

        public ICommand Load
        {
            get
            {
                if (this.loadCommand == null)
                {
                    this.loadCommand = new RelayCommand(this.HandleLoadCommand);
                }

                return this.loadCommand;
            }
        }

        #endregion


        #region Constructor

        public PlayGroundVM()
        {
            this.Background = new SolidColorBrush(Colors.Transparent);
            this.DrawableObjects = new ObservableCollection<ShapeViewModelBase>();

            this.timer.Interval = TimeSpan.FromSeconds(PlayGroundVM.TimerSecondsInterval);
            this.AttachTimerToEvent();
        }

        #endregion


        #region Methods
        
        private void HandleRestartCommand(object parameter = null)
        {
            double width = this.Width;
            double height = this.Height;

            this.GameLevel = this.gameLevel;

            this.FitInSize(width, height);
        }

        private async void HandleLoadCommand(object obj)
        {
            GameLevelModel gameLevel;
            if (LevelsManager.TryGetSaveGameFromRoaming(this.gameLevel.Name, out gameLevel))
            {
                this.gameLevel = gameLevel;
                this.HandleRestartCommand();
            }
            else
            {
                await new MessageDialog("No save game to load for this level name!").ShowAsync();
            }
        }

        private async void HandleSaveCommand(object obj)
        {
            GameLevelModel gameLevel = this.ToGameLevelModel();
            LevelsManager.AddSaveGameToRoaming(gameLevel);
            await new MessageDialog("Game successfully saved!").ShowAsync();
        }

        private GameLevelModel ToGameLevelModel()
        {
            GameLevelModel gameLevel = LevelsManager.Empty;

            gameLevel.Score = this.Score;
            gameLevel.Width = this.gameLevel.Width;
            gameLevel.Height = this.gameLevel.Height;
            gameLevel.Name = this.gameLevel.Name;
            gameLevel.GravityAcceleration = this.gameLevel.GravityAcceleration;

            double scaleFactor = gameLevel.Width / this.Width;

            gameLevel.Ball = this.Ball.ToModel(scaleFactor);

            List<GiftModel> gifts = new List<GiftModel>();
            foreach (var gift in this.Gifts)
            {
                gifts.Add(gift.ToModel(scaleFactor));
            }
            gameLevel.Gifts = gifts;

            List<ObstacleModel> obstacles = new List<ObstacleModel>();
            foreach (var obstacle in this.Obstacles)
            {
                if (!(obstacle.Equals(this.Playground) || obstacle == this.Playground))
                {
                    obstacles.Add(obstacle.ToModel(scaleFactor));
                }
            }
            gameLevel.Obstacles = obstacles;

            return gameLevel;
        }

        private void LoadGameLevel(GameLevelModel gameLevel)
        {
            this.gameLevel = gameLevel;
            this.Score = gameLevel.Score;
            this.Width = gameLevel.Width;
            this.Height = gameLevel.Height;
            this.Name = gameLevel.Name;
            this.GravityAcceleration = new Vector2D(0, gameLevel.GravityAcceleration);

            this.DrawableObjects.Clear();

            this.Obstacles = new List<ObstacleViewModel>();

            this.Playground = new ObstacleViewModel(0, 0, gameLevel.Height, gameLevel.Width, 0);
            this.DrawableObjects.Add(this.Playground);
            this.Obstacles.Add(this.Playground);

            foreach (var model in gameLevel.Obstacles)
            {
                ObstacleViewModel obstacle = model.ToViewModel();
                this.DrawableObjects.Add(obstacle);
                this.Obstacles.Add(obstacle);
            }

            this.Gifts = new List<GiftViewModel>();
            foreach (var model in gameLevel.Gifts)
            {
                GiftViewModel gift = model.ToViewModel();
                this.DrawableObjects.Add(gift);
                this.Gifts.Add(gift);
            }

            this.Ball = gameLevel.Ball.ToViewModel();
            this.DrawableObjects.Add(this.Ball);
        }

        public void Scale(double scaleFactor)
        {
            this.Width *= scaleFactor;
            this.Height *= scaleFactor;
            this.GravityAcceleration.Scale(scaleFactor);

            foreach (var drawable in this.DrawableObjects)
            {
                drawable.Scale(scaleFactor);
            }
        }

        public void FitInSize(double maxWidth, double maxHeight)
        {
            double scaleFactor = (maxWidth / this.Width) * this.Height <= maxHeight
                ? (maxWidth / this.Width)
                : (maxHeight / this.Height);

            this.Scale(scaleFactor);
        }
        
        private void TryMoveBallLinear(double seconds)
        {
            this.StopTimer();
            while (seconds > 0)
            {
                double distance;
                Vector2D normal;
                if (this.Ball.TryCollide(this.GetAllObstacleLines(), seconds, out distance, out normal))
                {
                    if (this.Ball.Speed.X.IsZero() && normal.X.IsZero() && normal.Y < 0)
                    {
                        this.shouldAddGravity = false;
                        this.Ball.Speed = new Vector2D(0, Math.Abs(this.Ball.Speed.Y) * Ball.NormalSpeedLoss);
                    }

                    if (!distance.IsZero())
                    {
                        this.RemoveCollidingGifts(distance);

                        double usedSeconds;
                        this.Ball.Move(distance, out usedSeconds);
                        if (this.shouldAddGravity)
                        {
                            this.Ball.Speed += this.GravityAcceleration * usedSeconds;
                        }
                        seconds -= usedSeconds;
                    }                        
                }
                else
                {
                    distance = seconds * this.Ball.Speed.Length;
                    this.RemoveCollidingGifts(distance);

                    this.Ball.Move(seconds);
                    if (this.shouldAddGravity)
                    {
                        this.Ball.Speed += this.GravityAcceleration * seconds;
                    }
                    seconds = 0;
                }
            }

            if (this.Ball.IsMoving || this.shouldAddGravity)
            {
                this.StartTimer();
            }
        }

        private void AttachTimerToEvent()
        {           
            this.timer.Tick += Timer_Tick;
        }

        private void DetachTimerFromEvent()
        {
            this.timer.Tick -= Timer_Tick;
        }

        void Timer_Tick(object sender, object e)
        {
            this.TryMoveBallLinear(PlayGroundVM.TimerSecondsInterval);
        }

        public void StartMovingBall(Vector2D unitsPerSecondStartSpeed)
        {
            this.StopTimer();

            this.Ball.Speed += unitsPerSecondStartSpeed;

            this.shouldAddGravity = true;

            this.StartTimer();
        }

        private IEnumerable<LineSegment2D> GetAllObstacleLines()
        {
            foreach (var obstacle in this.Obstacles)
            {
                foreach (var side in obstacle.Sides)
                {
                    yield return side;
                }
            }
        }               

        private void RemoveCollidingGifts(double maxDistance)
        {
            List<GiftViewModel> giftsToRemove = new List<GiftViewModel>();

            for (int i = 0; i < this.Gifts.Count; i++ )
            {
                double distance;
                Vector2D normal;
                if (this.Ball.TryCollide(this.Gifts[i].Sides, PlayGroundVM.TimerSecondsInterval, out distance, out normal))
                {
                    if (distance <= maxDistance)
                    {                        
                        giftsToRemove.Add(this.Gifts[i]);
                    }
                }
            }

            for (int i = 0; i < giftsToRemove.Count; i++)
            {
                this.DrawableObjects.Remove(giftsToRemove[i]);
                this.Gifts.Remove(giftsToRemove[i]);
                this.OnGiftCollected();
            }
        }

        private void OnGiftCollected()
        {
            this.Score += 100;
        }

        public void OnManipulationStarted()
        {
            this.Score -= 20;
        }

        public bool CanBallGoDown()
        {
            double nearest;
            Vector2D normal;
            if (this.Ball.TryCollide(this.GetAllObstacleLines(), 1, out nearest, out normal))
            {
                if (nearest.IsZero())
                {
                    return normal.Y > 0;
                }
            }

            return true;
        }

        public bool CanMoveBall(Vector2D startPoint, Vector2D endPoint)
        {
            Vector2D difference = endPoint - startPoint;
            if (difference.Length >= 2 * this.Ball.Radius)
            {
                return false;
            }

            Circle circle = new Circle(endPoint, this.Ball.Circle.Radius);

            foreach (var segment in this.GetAllObstacleLines())
            {
                var intersections = segment.IntersectWith(circle);
                if (intersections != null && intersections.Length > 0)
                {
                    return false;
                }
            }

            return true;
        }

        public void StopTimer()
        {
            if (this.timer.IsEnabled)
            {
                this.timer.Stop();
            }
        }

        public void StartTimer()
        {
            if (!this.timer.IsEnabled)
            {
                this.timer.Start();
            }
        }
        
        public void Dispose()
        {
            this.timer.Stop();
            this.DetachTimerFromEvent();
        }

        #endregion
    }
}
